package org.teamh.gui.engine.ogg;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.FloatBuffer;
import java.nio.IntBuffer;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;

import org.lwjgl.BufferUtils;
import org.lwjgl.openal.AL;
import org.lwjgl.openal.AL10;
import org.lwjgl.openal.ALC10;
import org.lwjgl.openal.ALCcontext;
import org.lwjgl.openal.ALCdevice;
import org.lwjgl.openal.OpenALException;

public class SoundCache {

	private boolean soundworks = false;

	private int sourceCount;

	/** List of loaded files */
	private static HashMap<String, Audio> loadedFiles = new HashMap<String, Audio>();

	/** List of sources */
	private static ArrayList<Integer> sources = new ArrayList<Integer>();

	private int maxSources = 64;

	private boolean inited = false;

	private static SoundCache self = new SoundCache();

	private SoundCache() {

	}

	public static SoundCache get() {

		return self;

	}

	public void init() {

		if (inited) {

			return;

		}

		inited = true;

		AccessController.doPrivileged(new PrivilegedAction<Object>() {

			public Object run() {

				try {

					AL.create();

					soundworks = true;

				} catch (Exception e) {

					// GameWindow.close();TODO

				}

				return null;

			}

		});

		if (soundworks) {

			sourceCount = 0;

			while (AL10.alGetError() == AL10.AL_NO_ERROR) {

				IntBuffer temp = BufferUtils.createIntBuffer(1);

				try {

					AL10.alGenSources(temp);

					if (AL10.alGetError() == AL10.AL_NO_ERROR) {

						sourceCount++;
						sources.add(temp.get(0));

						if (sourceCount > maxSources - 1) {

							break;

						}
					}

				} catch (OpenALException e) {
					// expected at the end
					break;
				}
			}

			if (AL10.alGetError() != AL10.AL_NO_ERROR) {

				soundworks = false;
				// GameWindow.close();TODO

			} else {

				FloatBuffer listenerOri = BufferUtils.createFloatBuffer(6).put(
						new float[] { 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f });
				FloatBuffer listenerVel = BufferUtils.createFloatBuffer(3).put(
						new float[] { 0.0f, 0.0f, 0.0f });
				FloatBuffer listenerPos = BufferUtils.createFloatBuffer(3).put(
						new float[] { 0.0f, 0.0f, 0.0f });
				listenerPos.flip();
				listenerVel.flip();
				listenerOri.flip();
				AL10.alListener(AL10.AL_POSITION, listenerPos);
				AL10.alListener(AL10.AL_VELOCITY, listenerVel);
				AL10.alListener(AL10.AL_ORIENTATION, listenerOri);

			}
		}

	}

	/**
	 * Get the Sound based on a specified OGG file
	 * 
	 * @param ref
	 *            The reference to the OGG file in the classpath
	 * @return The Sound read from the OGG file
	 * @throws IOException
	 *             Indicates a failure to load the OGG
	 */
	public static Audio getOgg(String ref) {
		SoundCache.get().init();

		return SoundCache.get().getOgg(ref,
				SoundCache.get().getResourceAsStream(ref));
	}

	/**
	 * Get a resource
	 * 
	 * @param ref
	 *            The reference to the resource to retrieve
	 * @return A stream from which the resource can be read
	 */
	private InputStream getResourceAsStream(String ref) {
		InputStream in = SoundCache.class.getClassLoader().getResourceAsStream(
				ref);

		if (in == null) {
			throw new RuntimeException("Resource not found: " + ref);
		}

		return new BufferedInputStream(in);
	}

	/**
	 * Get the Sound based on a specified OGG file
	 * 
	 * @param ref
	 *            The reference to the OGG file in the classpath
	 * @param in
	 *            The stream to the OGG to load
	 * @return The Sound read from the OGG file
	 * @throws IOException
	 *             Indicates a failure to load the OGG
	 */
	private Audio getOgg(String ref, InputStream in) {
		if (!soundworks) {
			System.out.println("sound does not work");
			return null;
		}

		if (!inited) {
			throw new RuntimeException(
					"Can't load sounds until SoundStore is init(). Use the container init() method.");
		}

		Audio ret = null;

		if (loadedFiles.get(ref) != null) {
			ret = loadedFiles.get(ref).clone();
		} else {
			try {
				IntBuffer buf = BufferUtils.createIntBuffer(1);

				OggDecoder decoder = new OggDecoder();
				OggData ogg = decoder.getData(in);

				AL10.alGenBuffers(buf);
				AL10.alBufferData(buf.get(0),
						ogg.channels > 1 ? AL10.AL_FORMAT_STEREO16
								: AL10.AL_FORMAT_MONO16, ogg.data, ogg.rate);

				ret = new Audio(buf.get(0));
				loadedFiles.put(ref, ret);

			} catch (Exception e) {

				System.out.println("Unable to load: " + ref);

			}
		}

		return ret;
	}

	/**
	 * Set the sound volume
	 * 
	 * @param volume
	 *            The volume for sound fx
	 */
	public void setVolume(float volume, int idx) {

		AL10.alSourcef(sources.get(idx), AL10.AL_GAIN, volume);

	}

	public void setLoop(boolean loop, int idx) {

		AL10.alSourcef(sources.get(idx), AL10.AL_LOOPING, loop ? AL10.AL_TRUE
				: AL10.AL_FALSE);

	}

	/**
	 * Set the pitch at which the current music is being played
	 * 
	 * @param pitch
	 *            The pitch at which the current music is being played
	 */
	public void setPitch(float pitch, int idx) {

		AL10.alSourcef(sources.get(idx), AL10.AL_PITCH, pitch);

	}

	public void setSourcePosition(float[] pos, int idx) {
		FloatBuffer position = BufferUtils.createFloatBuffer(3).put(pos);
		position.flip();

		AL10.alSource(sources.get(idx), AL10.AL_POSITION, position);
	}

	public void setSourceVelocity(float[] vel, int idx) {
		FloatBuffer velocity = BufferUtils.createFloatBuffer(3).put(vel);
		velocity.flip();

		AL10.alSource(sources.get(idx), AL10.AL_POSITION, velocity);
	}

	public void play(int idx) {

		if (soundworks) {

			AL10.alSourcePlay(sources.get(idx));

		}

	}

	public void play(int buffer, float pitch, float gain, boolean loop,
			int idx, float x, float y, float z) {

		if (soundworks) {

			AL10.alSourcei(sources.get(idx), AL10.AL_BUFFER, buffer);
			AL10.alSourcef(sources.get(idx), AL10.AL_PITCH, pitch);
			AL10.alSourcef(sources.get(idx), AL10.AL_GAIN, gain);
			AL10.alSourcei(sources.get(idx), AL10.AL_LOOPING,
					loop ? AL10.AL_TRUE : AL10.AL_FALSE);

			FloatBuffer velocity = BufferUtils.createFloatBuffer(3).put(
					new float[] { 0, 0, 0 });
			FloatBuffer position = BufferUtils.createFloatBuffer(3).put(
					new float[] { x, y, z });
			position.flip();
			velocity.flip();

			AL10.alSource(sources.get(idx), AL10.AL_POSITION, position);
			AL10.alSource(sources.get(idx), AL10.AL_VELOCITY, velocity);

			AL10.alSourcePlay(sources.get(idx));

		}

	}

	public void stop(int idx) {

		AL10.alSourceStop(sources.get(idx));

	}

	public void pause(int idx) {

		AL10.alSourcePause(sources.get(idx));

	}

	public void setListenerPosition(float[] pos) {

		// BackgroundMusic.get().setPosition(pos);TODO

		FloatBuffer listenerPos = BufferUtils.createFloatBuffer(3).put(pos);
		listenerPos.flip();
		AL10.alListener(AL10.AL_POSITION, listenerPos);

	}

	public void setListenerOrientation(float[] pos) {

		FloatBuffer listenerOri = BufferUtils.createFloatBuffer(6).put(pos);
		listenerOri.flip();
		AL10.alListener(AL10.AL_ORIENTATION, listenerOri);

	}

	/**
	 * Find a free sound source
	 * 
	 * @return The index of the free sound source
	 */
	public int findFreeSource() {
		for (int i = 1; i < sourceCount - 1; i++) {
			int state = AL10.alGetSourcei(sources.get(i), AL10.AL_SOURCE_STATE);

			if ((state != AL10.AL_PLAYING) && (state != AL10.AL_PAUSED)) {
				return i;
			}
		}

		return -1;
	}

	public boolean isPlaying(int idx) {
		int state = AL10.alGetSourcei(idx, AL10.AL_SOURCE_STATE);

		if ((state != AL10.AL_PLAYING) && (state != AL10.AL_PAUSED)) {
			return true;
		}

		return false;
	}

	/**
	 * 1) Releases all buffers. 2) Releases all sources.
	 */
	public void destroy() {
		IntBuffer scratch = BufferUtils.createIntBuffer(1);

		// Release all source data.
		for (Iterator<Integer> iter = sources.iterator(); iter.hasNext();) {
			scratch.put(0, ((Integer) iter.next()).intValue());
			do {
				AL10.alSourceStop(scratch);
			} while (AL10.alGetSourcei(scratch.get(0), AL10.AL_SOURCE_STATE) == AL10.AL_PLAYING);

			AL10.alDeleteSources(scratch);
		}

		// Release all buffer data.
		for (Audio i : loadedFiles.values()) {
			scratch.put(0, i.getIndex());
			AL10.alDeleteBuffers(scratch);
		}

		// Destroy the lists.
		sources.clear();
		loadedFiles.clear();

		ALCcontext context;
		ALCdevice device;

		context = ALC10.alcGetCurrentContext();

		// Get device for active context
		device = ALC10.alcGetContextsDevice(context);

		// Disable context
		ALC10.alcMakeContextCurrent(null);

		// Release context(s)
		ALC10.alcDestroyContext(context);

		// Close device
		ALC10.alcCloseDevice(device);
	}
}
